/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.os890.ds.addon.test.api.junit; import org.apache.deltaspike.cdise.api.CdiContainer; import org.apache.deltaspike.cdise.api.CdiContainerLoader; import org.apache.deltaspike.cdise.api.ContextControl; import org.apache.deltaspike.core.api.projectstage.ProjectStage; import org.apache.deltaspike.core.api.provider.BeanManagerProvider; import org.apache.deltaspike.core.api.provider.BeanProvider; import org.apache.deltaspike.core.util.ExceptionUtils; import org.apache.deltaspike.core.util.ProjectStageProducer; import org.apache.deltaspike.core.util.ServiceUtils; import org.junit.Test; import org.junit.internal.runners.statements.FailOnTimeout; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; import org.os890.ds.addon.test.api.TestControl; import org.os890.ds.addon.test.api.literal.TestControlLiteral; import org.os890.ds.addon.test.spi.junit.TestStatementHandler; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.enterprise.context.RequestScoped; import javax.enterprise.context.SessionScoped; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.inject.Singleton; import java.lang.annotation.Annotation; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.Logger; public class CdiTestRunner extends BlockJUnit4ClassRunner { private static final Logger LOGGER = Logger.getLogger(CdiTestRunner.class.getName()); private List<TestStatementHandler> statementHandlers; private ContainerAwareTestContext testContext; private static Set<Integer> notifierIdentities = new CopyOnWriteArraySet<Integer>(); public CdiTestRunner(Class<?> testClass) throws InitializationError { super(testClass); TestControl testControl = testClass.getAnnotation(TestControl.class); this.testContext = new ContainerAwareTestContext(testControl, null); //benefits from the fallback-handling in ContainerAwareTestContext Class<? extends Handler> logHandlerClass = testContext.getLogHandlerClass(); if (!Handler.class.equals(logHandlerClass)) { try { LOGGER.addHandler(logHandlerClass.newInstance()); } catch (Exception e) { throw ExceptionUtils.throwAsRuntimeException(e); } } this.statementHandlers = ServiceUtils.loadServiceImplementations(TestStatementHandler.class); Collections.sort(this.statementHandlers, new Comparator<TestStatementHandler>() { @Override public int compare(TestStatementHandler h1, TestStatementHandler h2) { return h1.getOrdinal() > h2.getOrdinal() ? 1 : -1; } }); } @Override public void run(RunNotifier runNotifier) { if (!CdiTestSuiteRunner.isContainerStarted()) //not called as a part of a test-suite { int identityHashCode = System.identityHashCode(runNotifier); if (!notifierIdentities.contains(identityHashCode)) { addLogRunListener(runNotifier, identityHashCode); } } super.run(runNotifier); } private synchronized void addLogRunListener(RunNotifier notifier, int identityHashCode) { if (notifierIdentities.contains(identityHashCode)) { return; } notifierIdentities.add(identityHashCode); notifier.addListener(new CdiTestSuiteRunner.LogRunListener()); } @Override protected Statement methodInvoker(FrameworkMethod method, Object test) { return new ContainerAwareMethodInvoker(method, test); } @Override protected void runChild(FrameworkMethod method, RunNotifier notifier) { TestControl testControl = method.getAnnotation(TestControl.class); ContainerAwareTestContext currentTestContext = new ContainerAwareTestContext(testControl, this.testContext); currentTestContext.applyBeforeMethodConfig(); try { super.runChild(method, notifier); } finally { currentTestContext.applyAfterMethodConfig(); } } //TODO use Rules instead @Override protected Statement withPotentialTimeout(FrameworkMethod method, Object test, Statement next) { Statement result = super.withPotentialTimeout(method, test, next); if (result instanceof FailOnTimeout) { return new Statement() { @Override public void evaluate() throws Throwable { throw new RuntimeException("@" + Test.class.getName() + "#timeout isn't supported"); } }; } return result; } //TODO use Rules instead @Override protected Statement withBefores(FrameworkMethod method, Object target, Statement statement) { Statement result = super.withBefores(method, target, statement); result = wrapBeforeStatement(result, getTestClass(), target); return result; } private Statement wrapBeforeStatement(Statement statement, TestClass testClass, Object target) { for (TestStatementHandler statementHandler : this.statementHandlers) { Statement result = statementHandler.createBeforeStatement(statement, testClass, target); if (result != null) { statement = result; } } return statement; } //TODO use Rules instead @Override protected Statement withAfters(FrameworkMethod method, final Object target, final Statement statement) { Statement result = super.withAfters(method, target, statement); result = wrapAfterStatement(result, getTestClass(), target); return result; } private Statement wrapAfterStatement(Statement statement, TestClass testClass, Object target) { for (TestStatementHandler statementHandler : this.statementHandlers) { Statement result = statementHandler.createAfterStatement(statement, testClass, target); if (result != null) { statement = result; } } return statement; } @Override protected Statement withBeforeClasses(Statement statement) { return new BeforeClassStatement(super.withBeforeClasses(statement), this.testContext); } @Override protected Statement withAfterClasses(Statement statement) { Statement result = super.withAfterClasses(statement); if (!CdiTestSuiteRunner.isContainerStarted()) { return new AfterClassStatement(result, this.testContext, notifierIdentities); } return result; } private class ContainerAwareMethodInvoker extends Statement { private final FrameworkMethod method; private final Object originalTarget; public ContainerAwareMethodInvoker(FrameworkMethod method, Object originalTarget) { this.method = method; this.originalTarget = originalTarget; } @Override public void evaluate() throws Throwable { BeanManager beanManager = BeanManagerProvider.getInstance().getBeanManager(); Class<?> type = this.method.getMethod().getDeclaringClass(); Set<Bean<?>> beans = beanManager.getBeans(type); if (beans == null || beans.isEmpty()) { BeanProvider.injectFields(this.originalTarget); //fallback to simple injection invokeMethod(this.originalTarget); } else { Bean<Object> bean = (Bean<Object>) beanManager.resolve(beans); CreationalContext<Object> creationalContext = beanManager.createCreationalContext(bean); Object target = beanManager.getReference(bean, type, creationalContext); try { invokeMethod(target); } finally { if (bean.getScope().equals(Dependent.class)) { bean.destroy(target, creationalContext); } } } } private void invokeMethod(Object target) { try { this.method.invokeExplosively(target); } catch (Throwable throwable) { throw ExceptionUtils.throwAsRuntimeException(throwable); } } } private class BeforeClassStatement extends Statement { private final Statement wrapped; private final ContainerAwareTestContext testContext; BeforeClassStatement(Statement statement, ContainerAwareTestContext testContext) { this.wrapped = statement; this.testContext = testContext; } @Override public void evaluate() throws Throwable { testContext.applyBeforeClassConfig(); wrapped.evaluate(); } } private class AfterClassStatement extends Statement { private final Statement wrapped; private final ContainerAwareTestContext testContext; private Set<Integer> notifierIdentities; public AfterClassStatement(Statement statement, ContainerAwareTestContext testContext, Set<Integer> notifierIdentities) { this.wrapped = statement; this.testContext = testContext; this.notifierIdentities = notifierIdentities; } @Override public void evaluate() throws Throwable { notifierIdentities.clear(); try { wrapped.evaluate(); } finally { testContext.applyAfterClassConfig(); } } } private static class ContainerAwareTestContext { private ContainerAwareTestContext parent; private final ProjectStage projectStage; private final TestControl testControl; private ProjectStage previousProjectStage; private boolean containerStarted = false; //only true for the layer it was started in private Stack<Class<? extends Annotation>> startedScopes = new Stack<Class<? extends Annotation>>(); ContainerAwareTestContext(TestControl testControl, ContainerAwareTestContext parent) { this.parent = parent; Class<? extends ProjectStage> foundProjectStageClass; if (testControl == null) { this.testControl = new TestControlLiteral(); if (parent != null) { foundProjectStageClass = parent.testControl.projectStage(); } else { foundProjectStageClass = this.testControl.projectStage(); } } else { this.testControl = testControl; foundProjectStageClass = this.testControl.projectStage(); } this.projectStage = ProjectStage.valueOf(foundProjectStageClass.getSimpleName()); } protected boolean isContainerStarted() { return this.containerStarted || (this.parent != null && this.parent.isContainerStarted()) || CdiTestSuiteRunner.isContainerStarted(); } public Class<? extends Handler> getLogHandlerClass() { return this.testControl.logHandler(); } public void applyBeforeClassConfig() { CdiContainer container = CdiContainerLoader.getCdiContainer(); if (!isContainerStarted()) { container.boot(); setContainerStarted(); } List<Class<? extends Annotation>> restrictedScopes = new ArrayList<Class<? extends Annotation>>(); //controlled by the container and not supported by weld: restrictedScopes.add(ApplicationScoped.class); restrictedScopes.add(Singleton.class); if (this.parent == null && this.testControl.getClass().equals(TestControlLiteral.class)) { //skip scope-handling if @TestControl isn't used explicitly on the test-class -> TODO re-visit it restrictedScopes.add(RequestScoped.class); restrictedScopes.add(SessionScoped.class); } startContexts(container, restrictedScopes.toArray(new Class[restrictedScopes.size()])); } public void applyAfterClassConfig() { CdiContainer container = CdiContainerLoader.getCdiContainer(); stopStartedScopes(); if (this.containerStarted) { //stop the container on the same level which started it container.shutdown(); } } public void applyBeforeMethodConfig() { this.previousProjectStage = ProjectStageProducer.getInstance().getProjectStage(); ProjectStageProducer.setProjectStage(this.projectStage); startContexts(CdiContainerLoader.getCdiContainer()); } public void applyAfterMethodConfig() { try { stopStartedScopes(); } finally { ProjectStageProducer.setProjectStage(previousProjectStage); previousProjectStage = null; } } public void setContainerStarted() { this.containerStarted = true; } private void startContexts(CdiContainer container, Class<? extends Annotation>... restrictedScopes) { ContextControl contextControl = container.getContextControl(); List<Class<? extends Annotation>> scopeClasses = new ArrayList<Class<? extends Annotation>>(); Collections.addAll(scopeClasses, this.testControl.startScopes()); if (this.testControl.startScopes().length == 0) { addScopesForDefaultBehavior(scopeClasses); } for (Class<? extends Annotation> scopeAnnotation : scopeClasses) { if (this.parent != null && this.parent.isScopeStarted(scopeAnnotation)) { continue; } if (isRestrictedScope(scopeAnnotation, restrictedScopes)) { continue; } try { contextControl.stopContext(scopeAnnotation); //force a clean context contextControl.startContext(scopeAnnotation); this.startedScopes.add(scopeAnnotation); } catch (RuntimeException e) { Logger logger = Logger.getLogger(CdiTestRunner.class.getName()); logger.setLevel(Level.SEVERE); logger.log(Level.SEVERE, "failed to start scope @" + scopeAnnotation.getName(), e); } } } private void addScopesForDefaultBehavior(List<Class<? extends Annotation>> scopeClasses) { if (this.parent != null && !this.parent.isScopeStarted(SessionScoped.class)) { if (!scopeClasses.contains(SessionScoped.class)) { scopeClasses.add(SessionScoped.class); } } if (this.parent != null && !this.parent.isScopeStarted(RequestScoped.class)) { if (!scopeClasses.contains(RequestScoped.class)) { scopeClasses.add(RequestScoped.class); } } } private boolean isRestrictedScope(Class<? extends Annotation> scopeAnnotation, Class<? extends Annotation>[] restrictedScopes) { for (Class<? extends Annotation> restrictedScope : restrictedScopes) { if (scopeAnnotation.equals(restrictedScope)) { return true; } } return false; } private boolean isScopeStarted(Class<? extends Annotation> scopeAnnotation) { return this.startedScopes.contains(scopeAnnotation); } private void stopStartedScopes() { ContextControl contextControl = CdiContainerLoader.getCdiContainer().getContextControl(); while (!this.startedScopes.empty()) { Class<? extends Annotation> scopeAnnotation = this.startedScopes.pop(); //TODO check if context was started by parent try { contextControl.stopContext(scopeAnnotation); } catch (RuntimeException e) { Logger logger = Logger.getLogger(CdiTestRunner.class.getName()); logger.setLevel(Level.SEVERE); logger.log(Level.SEVERE, "failed to stop scope @" + scopeAnnotation.getName(), e); } } } } }